/*
 * Copyright (c) 2015, the DeltaSync# project. All Rights Reserved.
 * This project is a C# port of the JDeltaSync project:
 *     http://jdeltasync.googlecode.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Text;

/**
 * Utility methods for working with XML {@link Document}s.
 * <p>
 * Some of the methods take an XPath like <code>path</code> parameter. A path
 * only matches {@link Element}s and is always relative to a root {@link Node}.
 * Paths can only contain local names, qualified names, '/' and '//'. 
 * '/' is used to find an immediate child while '//' is used to find a 
 * descendant. Namespace prefixes can be used to match {@link Element} in a 
 * particular namespace. The special '*' prefix matches an {@link Element} in 
 * any namespace or without a namespace. The following namespace prefixes can be 
 * used:
 * <table>
 *   <tr>
 *     <th>Prefix</th><th>Namespace URI</th>
 *   </tr>
 *   <tr>
 *     <td>s</td><td>http://www.w3.org/2003/05/soap-envelope</td>
 *   </tr>
 *   <tr>
 *     <td>wsse</td><td>http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd</td>
 *   </tr>
 *   <tr>
 *     <td>wsu</td><td>http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd</td>
 *   </tr>
 *   <tr>
 *     <td>wst</td><td>http://schemas.xmlsoap.org/ws/2005/02/trust</td>
 *   </tr>
 *   <tr>
 *     <td>airsync</td><td>AirSync:</td>
 *   </tr>
 *   <tr>
 *     <td>itemop</td><td>ItemOperations:</td>
 *   </tr>
 *   <tr>
 *     <td>hmfolder</td><td>HMFOLDER:</td>
 *   </tr>
 *   <tr>
 *     <td>hmmail</td><td>HMMAIL:</td>
 *   </tr>
 *   <tr>
 *     <td>email</td><td>EMAIL:</td>
 *   </tr>
 *   <tr>
 *     <td>hmsync</td><td>HMSYNC:</td>
 *   </tr>
 * </table>
 */
internal class XmlUtil {
    private static Dictionary<string, string> NAMESPACES;
    private static Encoding                   UTF8NoBOM = new UTF8Encoding(false);
    
    static XmlUtil() {
        NAMESPACES = new Dictionary<string, string>();
        NAMESPACES.Add("s",    "http://www.w3.org/2003/05/soap-envelope");
        NAMESPACES.Add("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
        NAMESPACES.Add("wsu",  "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
        NAMESPACES.Add("wst",  "http://schemas.xmlsoap.org/ws/2005/02/trust");
        NAMESPACES.Add("psf",  "http://schemas.microsoft.com/Passport/SoapServices/SOAPFault");
        NAMESPACES.Add("airsync",  "AirSync:");
        NAMESPACES.Add("itemop",   "ItemOperations:");
        NAMESPACES.Add("hmfolder", "HMFOLDER:");
        NAMESPACES.Add("hmmail",   "HMMAIL:");
        NAMESPACES.Add("email",    "EMAIL:");
        NAMESPACES.Add("hmsync",   "HMSYNC:");
    }
    
    /**
     * Parses XML data from a stream and returns a {@link Document}.
     * 
     * @param input the stream to read from.
     * @return the parsed {@link Document}.
     */
    public static XmlDocument Parse(Stream input) {
        XmlDocument dom = new XmlDocument();
        dom.XmlResolver = null;
        dom.Load(input);
        return dom;
    }

    public static XmlDocument ParseXml(string input) {
        XmlDocument dom = new XmlDocument();
        dom.XmlResolver = null;
        dom.LoadXml(input);
        return dom;
    }

    private static List<string> split(string s, string delim, bool includeDelim) {
        /*
        StringTokenizer tok = new StringTokenizer(s, delim, includeDelim);
        string[] parts = new string[tok.countTokens()];
        for (int i = 0; tok.hasMoreTokens(); i++)
            parts[i] = tok.nextToken();
        return parts;
        */
           List<string> retval = new List<string>();
           foreach (string part in s.Split(new string[] { delim }, StringSplitOptions.None))
               retval.Add(part.Length == 0 ? delim : part);
           return retval;
    }
    
    /**
     * Returns the first {@link Element} which matches the specified
     * <code>path</code>. The search starts at the specified root {@link Node}.
     * 
     * @param root the {@link Node} where the search will start.
     * @param path the path to search for.
     * @return the first {@link Element} that matches the path or 
     *         <code>null</code> if no match could be found.
     */
    public static XmlElement getElement(XmlNode root, string path) {
        List<XmlElement> l = getElements(root, path);
        if (l.Count == 0)
            return null;
        return l[0];
    }
    
    /**
     * Returns all {@link Element}s which matches the specified
     * <code>path</code>. The search starts at the specified root {@link Node}.
     * 
     * @param root the {@link Node} where the search will start.
     * @param path the path to search for.
     * @return the {@link Element}s that matches the path.
     */
    public static List<XmlElement> getElements(XmlNode root, string path) {
        return getElements(root, split(path, "/", true));
    }
    
    private static List<XmlElement> find(XmlNode root, string nsUri, string localName, bool recursive) {
        List<XmlElement> result = new List<XmlElement>();
        XmlNodeList nl = root.ChildNodes;
        if (nl != null) {
            int len = nl.Count;
            for (int i = 0; i < len; i++) {
                XmlNode child = nl.Item(i);
                if (child is XmlElement) {
                    string childUri       = child.NamespaceURI;
                    string childLocalName = child.LocalName;
                    if (("*" == nsUri || nsUri == null && childUri == null || nsUri == childUri) 
                            && localName == childLocalName) {
                        result.Add((XmlElement) child);
                    } else if (recursive) {
                        result.AddRange(find(child, nsUri, localName, recursive));
                    }
                }
            }
        }
        return result;
    }
    
    private static List<XmlElement> getElements(XmlNode root, List<string> parts) {
        bool recursive = false;
        if (parts[0] == "/") {
            parts.RemoveAt(0);
            if (parts[0] == "/") {
                parts.RemoveAt(0);
                recursive = true;
            }
        }
        
        string   nsUri = null;
        string[] qname = split(parts[0], ":", false).ToArray();
        parts.RemoveAt(0);
        if (qname.Length > 1) {
            if ("*" == qname[0]) {
                nsUri = "*";
            } else {
                nsUri = NAMESPACES[qname[0]];
                if (nsUri == null)
                    throw new ArgumentException("Unbound prefix " + qname[0]);
            }
        }
        string localName = qname.Length > 1 ? qname[1] : qname[0];
        List<XmlElement> matches = find(root, nsUri, localName, recursive);
        if (matches.Count > 0) {
            if (parts.Count == 0) {
                return matches;
            } else {
                List<XmlElement> result = new List<XmlElement>(); 
                foreach (XmlElement match in matches)
                    result.AddRange(getElements(match, new List<string>(parts)));
                return result;
            }
        }
        return new List<XmlElement>();
    }
    
    /**
     * Returns <code>true</code> if there is at least one {@link Element} which 
     * matches the specified <code>path</code>. The search starts at the 
     * specified root {@link Node}.
     * 
     * @param root the {@link Node} where the search will start.
     * @param path the path to search for.
     * @return <code>true</code> if a match was found. <code>false</code>
     *         otherwise.
     */
    public static bool hasElement(XmlNode root, string path) {
        return getElement(root, path) != null;
    }
    
    /**
     * Returns the text content of the first {@link Element} which matches the 
     * specified <code>path</code>. The search starts at the specified root 
     * {@link Node}.
     * 
     * @param root the {@link Node} where the search will start.
     * @param path the path to search for.
     * @return the text content of the first {@link Element} that matches the 
     *         path or <code>null</code> if no match could be found.
     */
    public static string getTextContent(XmlNode root, string path) {
        XmlElement el = getElement(root, path);
        string s = el == null ? null : el.InnerText;
        return s != null ? s.Trim() : null;
    }
    
    public static string getTextContent(XmlNode node) {
        string s = node == null ? null : node.InnerText;
        return s != null ? s.Trim() : null;
    }
    
    /**
     * Sets the text content of the first {@link Element} which matches the 
     * specified <code>path</code>. The search starts at the specified root 
     * {@link Node}.
     * 
     * @param root the {@link Node} where the search will start.
     * @param path the path to search for.
     * @param value the new text content.
     */
    public static void setTextContent(XmlNode root, string path, string value) {
        XmlElement el = getElement(root, path);
        el.InnerText = value;
    }
    
    public static void setTextContent(XmlNode node, string value) {
        node.InnerText = value;
    }
    
    /**
     * Writes the specified {@link Document} to an {@link OutputStream} in 
     * compact format.
     * 
     * @param doc the {@link Document}.
     * @param out the stream to write to.
     */
    public static void writeDocument(XmlDocument doc, Stream output) {
        writeDocument(doc, output, true);
    }
    
    /**
     * Writes the specified {@link Document} to an {@link OutputStream} in 
     * the specified format.
     * 
     * @param doc the {@link Document}.
     * @param out the stream to write to.
     * @param compact if <code>true</code> the XML will be written in compact
     *        format without any extra whitespaces.
     */
    public static void writeDocument(XmlDocument doc, Stream output, bool compact) {
           XmlWriterSettings settings = new XmlWriterSettings();
           settings.Indent   = !compact;
           settings.Encoding = UTF8NoBOM;
           using (XmlWriter xmlwriter = XmlWriter.Create(output, settings))
               doc.WriteTo(xmlwriter);
    }
    
    /**
     * Writes the specified {@link Document} to a {@link Writer} in 
     * compact format.
     * 
     * @param doc the {@link Document}.
     * @param writer the {@link Writer} to write to.
     */
    public static void writeDocument(XmlDocument doc, TextWriter writer) {
        writeDocument(doc, writer, true);
    }
    
    /**
     * Writes the specified {@link Document} to a {@link Writer} in 
     * the specified format.
     * 
     * @param doc the {@link Document}.
     * @param out the stream to write to.
     * @param compact if <code>true</code> the XML will be written in compact
     *        format without any extra whitespaces.
     */
    public static void writeDocument(XmlDocument doc, TextWriter writer, bool compact) {
           XmlWriterSettings settings = new XmlWriterSettings();
           settings.Indent   = !compact;
           settings.Encoding = writer.Encoding;
           using (XmlWriter xmlwriter = XmlWriter.Create(writer, settings))
               doc.WriteTo(xmlwriter);
    }
    
    /**
     * Serializes a {@link Document} to a byte array in compact format.
     * 
     * @param doc the {@link Document}.
     * @return the byte array.
     */
    public static byte[] ToByteArray(XmlDocument doc) {
        return ToByteArray(doc, true);
    }
    
    /**
     * Serializes a {@link Document} to a byte array in the specified format.
     * 
     * @param doc the {@link Document}.
     * @param compact if <code>true</code> the XML serialized in compact
     *        format without any extra whitespaces.
     * @return the byte array.
     */
    public static byte[] ToByteArray(XmlDocument doc, bool compact) {
        MemoryStream baos = new MemoryStream();
        writeDocument(doc, baos, compact);
        return baos.ToArray();
    }
    
    /**
     * Serializes a {@link Document} to a {@link string} in compact format.
     * 
     * @param doc the {@link Document}.
     * @return the {@link string}.
     */
    public static string ToString(XmlDocument doc) {
        return ToString(doc, true);
    }
    
    /**
     * Serializes a {@link Document} to a {@link string} in the specified format.
     * 
     * @param doc the {@link Document}.
     * @param compact if <code>true</code> the XML serialized in compact
     *        format without any extra whitespaces.
     * @return the {@link string}.
     */
    public static string ToString(XmlDocument doc, bool compact) {
        StringWriter sw = new StringWriter();
        writeDocument(doc, sw, compact);
        return sw.ToString();
    }
    
    public static T Parse<T>(string Value, XmlTypeCode TypeCode) {
        var type     = XmlSchemaType.GetBuiltInSimpleType(TypeCode);
           var datatype = type.Datatype;
           return (T)datatype.ChangeType(Value, typeof(T));
    }
    public static string Format(object Value, XmlTypeCode TypeCode) {
        if (Value == null)
            return null;
        var type     = XmlSchemaType.GetBuiltInSimpleType(TypeCode);
           var datatype = type.Datatype;
           return (string)datatype.ChangeType(Value, typeof(string));
    }
    
    public static int ParseInt(string Value) {
        return Parse<int>(Value, XmlTypeCode.Int);
    }
    public static long ParseLong(string Value) {
        return Parse<long>(Value, XmlTypeCode.Long);
    }
    public static DateTime ParseDateTime(string Value) {
        return Parse<DateTime>(Value, XmlTypeCode.DateTime);
    }
    
    public static string FormatDateTime(DateTime Value) {
        return Format(Value, XmlTypeCode.DateTime);
    }
    
    public static DateTime TruncateMillis(DateTime value) {
        return new DateTime(value.Ticks - (value.Ticks % TimeSpan.TicksPerSecond), value.Kind);
    }
}
